unit mapviewframeunit;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, ExtCtrls, Graphics, StdCtrls, fpjson,
  database, mapdrawerunit, tilesetgraphicsunit, BGRABitmapTypes,
  BGRABitmap, tileselectionunit, LCLTranslator;

type

  TMapViewFrameMode = (mvfmEdit, mvfmSelectLocation);

  TSelectMapViewFrameDoubleClickCallback = procedure of object;

  { TMapViewFrame }

  TMapViewFrame = class(TFrame)
    MapScrollBox: TScrollBox;
    MapPaintBox: TPaintBox;
    procedure MapPaintBoxDblClick(Sender: TObject);
    procedure MapPaintBoxMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure MapPaintBoxMouseLeave(Sender: TObject);
    procedure MapPaintBoxMouseMove(Sender: TObject; Shift: TShiftState; X,
      Y: Integer);
    procedure MapPaintBoxMouseUp(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure MapPaintBoxPaint(Sender: TObject);
  private
    FMode: TMapViewFrameMode;
    Map: TJSONObject;
    Db: TDatabase;
    MapImage: TBGRABitmap;
    MapImageX, MapImageY: Integer;
    MapWidth, MapHeight: Integer;
    DstTileWidth, DstTileHeight: Integer;
    Drawer: TMapDrawer;
    TilesetGraphics: TTilesetGraphics;
    OldClientRect: TRect;
    Selection: TTileSelection; //freed in the main window
    SelX, SelY, OldSelX, OldSelY: Integer;
    FEventCursorX, FEventCursorY: Integer;
    SelectedLayer: Integer;

    IsRTL: Boolean;

    IsDrawing: Boolean;
    PreciseDrawing: Boolean;
    StartX, StartY: Integer;
    function GetShiftedClientRect: TRect;
    function GetMapRectInTiles: TRect;
    procedure DrawEvents;
    function FindEventAt(X, Y: Integer): Integer;
    procedure SetMode(AValue: TMapViewFrameMode);
  public
    OnDoubleClick: TSelectMapViewFrameDoubleClickCallback;
    property Mode: TMapViewFrameMode read FMode write SetMode;
    property EventCursorX: Integer read FEventCursorX write FEventCursorX;
    property EventCursorY: Integer read FEventCursorY write FEventCursorY;
    constructor Create(TheOwner: TComponent); override;
    procedure SetMap(AMap: TJSONObject; ATilesetGraphics: TTilesetGraphics; ADb: TDatabase);
    procedure UpdateMapSize(NeedsRepaint: Boolean = True);
    procedure UpdateMapImage;
    procedure UpdatePartOfMapImage(PartInTiles: TRect);
    procedure SetZoom(Zoom: Integer);
    procedure SetSelection(ASelection: TTileSelection);
    procedure SelectMapLayer(LayerId: Integer);
    procedure UpdateLanguage(Code: String);
    destructor Destroy; override;
  end;

const
  Zooms: array [1..9] of Integer = (
    12, 16, 24, 32,
    48,
    64, 80, 96, 120
  );

implementation

uses
  Dialogs, mapedithelper, constants, eventeditorunit, languagehelper;

{$R *.lfm}

procedure TMapViewFrame.MapPaintBoxPaint(Sender: TObject);
var
  ShiftedClientRect: TRect;

  procedure DrawCursor(X, Y, Width, Height: Integer);
  begin
    MapPaintBox.Canvas.Pen.Width := 3;
    MapPaintBox.Canvas.Pen.Color := clWhite;
    MapPaintBox.Canvas.Brush.Style := bsClear;
    MapPaintBox.Canvas.Rectangle(
      X * DstTileWidth + 1,
      Y * DstTileHeight + 1,
      (X + Width) * DstTileWidth -2,
      (Y + Height) * DstTileHeight -2
    );

    MapPaintBox.Canvas.Pen.Width := 1;
    MapPaintBox.Canvas.Pen.Color := clBlack;
    MapPaintBox.Canvas.Rectangle(
    X * DstTileWidth + 1,
    Y * DstTileHeight + 1,
    (X + Width) * DstTileWidth -2,
    (Y + Height) * DstTileHeight -2
    );
  end;

begin
  ShiftedClientRect := GetShiftedClientRect;
  if OldClientRect <> ShiftedClientRect then begin
    UpdateMapImage;
    OldClientRect := ShiftedClientRect;
  end;

  MapPaintBox.Canvas.Brush.Color := clForm;
  MapPaintBox.Canvas.FillRect(0, 0, MapPaintBox.Width, MapPaintBox.Height);
  if (TilesetGraphics <> nil) and (MapImage <> nil) then
    MapImage.Draw(MapPaintBox.Canvas, MapImageX, MapImageY);

  if (SelectedLayer = LAYER_EVENTS) then begin
    DrawEvents;
    if (FEventCursorX > -1) and (FEventCursorY > -1) then
      DrawCursor(FEventCursorX, FEventCursorY, 1, 1)
  end else if (SelX > -1) and (SelY > -1) and (Selection <> nil) then begin
    DrawCursor(SelX, SelY, Selection.Width, Selection.Height);

    OldSelX := SelX;
    OldSelY := SelY;
  end;
end;

procedure TMapViewFrame.MapPaintBoxMouseLeave(Sender: TObject);
begin
  SelX := -1;
  SelY := -1;
  OldSelX := -1;
  OldSelY := -1;
  if Selection <> nil then
    Refresh;
end;

procedure TMapViewFrame.MapPaintBoxMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  TargetX, TargetY: Integer;
begin
  //TODO: start draw
  if (SelectedLayer <> LAYER_EVENTS) and (ssLeft in Shift) then begin
    IsDrawing := True;
    TargetX := X div DstTileWidth;
    TargetY := Y div DstTileHeight;
    StartX := TargetX;
    StartY := TargetY;
    PreciseDrawing := ssShift in Shift;
    if Selection <> nil then begin
      PlaceMapPencil(Map, Selection, TargetX, TargetY, 0, 0, SelectedLayer,
                          PreciseDrawing);
      UpdatePartOfMapImage(Rect(TargetX-1, TargetY-1, TargetX+Selection.Width, TargetY+Selection.Height));
      Refresh;
    end;
  end;
end;

procedure TMapViewFrame.MapPaintBoxDblClick(Sender: TObject);
var
  EventId: Integer;
begin
  if (SelectedLayer = LAYER_EVENTS) and (Mode = mvfmEdit) then begin
    EventId := FindEventAt(FEventCursorX, FEventCursorY);

    if EventId > 0 then
      EventEditorForm.EditEvent(Map.Arrays['events'], EventId, TilesetGraphics)
    else
      EventEditorForm.CreateNew(Map.Arrays['events'],
                                FEventCursorX, FEventCursorY,
                                TilesetGraphics);
  end else if Mode = mvfmSelectLocation then begin
    OnDoubleClick();
  end;
end;

procedure TMapViewFrame.MapPaintBoxMouseMove(Sender: TObject;
  Shift: TShiftState; X, Y: Integer);
var
  SelectionDiffers: Boolean;
begin
  SelX := X div DstTileWidth;
  SelY := Y div DstTileHeight;
  SelectionDiffers := (SelY <> OldSelX) or (SelY <> OldSelY);
  if IsDrawing and SelectionDiffers and (Selection <> nil) then begin
    PlaceMapPencil(Map, Selection, SelX, SelY,
                        SelX - StartX, SelY - StartY, SelectedLayer,
                        PreciseDrawing);
    UpdatePartOfMapImage(Rect(SelX-1, SelY-1, SelX+Selection.Width, SelY+Selection.Height));
    Refresh;
  end
  else if (Selection <> nil) and SelectionDiffers then
    Refresh;
end;

procedure TMapViewFrame.MapPaintBoxMouseUp(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  SelectionDiffers: Boolean;
begin
  SelX := X div DstTileWidth;
  SelY := Y div DstTileHeight;
  SelectionDiffers := (SelY <> OldSelX) or (SelY <> OldSelY);
  if IsDrawing and SelectionDiffers and (Selection <> nil) then begin
    PlaceMapPencil(Map, Selection, SelX, SelY,
                        SelX - StartX, SelY - StartY, SelectedLayer,
                        PreciseDrawing);
    UpdatePartOfMapImage(Rect(SelX-1, SelY-1, SelX+Selection.Width, SelY+Selection.Height));
    Refresh;
  end;
  IsDrawing := False;

  if SelectedLayer = LAYER_EVENTS then begin
    FEventCursorX := SelX;
    FEventCursorY := SelY;
    Refresh;

    if Button = mbRight then begin
      //TODO: show event menu
    end;
  end;
end;

function TMapViewFrame.GetShiftedClientRect: TRect;
begin
    GetShiftedClientRect := Rect(
      MapScrollBox.ClientRect.Left + MapScrollBox.HorzScrollBar.Position,
      MapScrollBox.ClientRect.Top + MapScrollBox.VertScrollBar.Position,
      MapScrollBox.ClientRect.Right + MapScrollBox.HorzScrollBar.Position,
      MapScrollBox.ClientRect.Bottom + MapScrollBox.VertScrollBar.Position
    );
end;

function TMapViewFrame.GetMapRectInTiles: TRect;
var
  RectShifted: TRect;
  RectInTiles: TRect;
begin
  RectShifted := GetShiftedClientRect;
  with RectShifted do begin
    RectInTiles.Left := Left div DstTileWidth;
    if Left mod DstTileWidth <> 0 then
      Dec(RectInTiles.Left);

    RectInTiles.Top := Top div DstTileHeight;
    if Top mod DstTileHeight <> 0 then
      Dec(RectInTiles.Top);

    RectInTiles.Right := Right div DstTileWidth;
    if Right mod DstTileWidth <> 0 then
      Inc(RectInTiles.Right);

    RectInTiles.Bottom := Bottom div DstTileHeight;
    if Bottom mod DstTileHeight <> 0 then
      Inc(RectInTiles.Bottom);
  end;

  GetMapRectInTiles := RectInTiles
end;

procedure TMapViewFrame.DrawEvents;
var
  Events: TJSONArray = nil;
  Event: TJSONObject;
  XJson: TJSONNumber = nil;
  YJson: TJSONNumber = nil;
  X, Y: Integer;
  EventRect: TRect;

  I: Integer;
begin
  //TODO
  MapPaintBox.Canvas.Pen.Width := 1;
  MapPaintBox.Canvas.Pen.Color := clNavy;
  MapPaintBox.Canvas.Brush.Style := bsFDiagonal;
  MapPaintBox.Canvas.Brush.Color := clTeal;

  if (Map <> nil) and Map.Find('events', Events) then
    for I := 1 to Events.Count - 1 do
      if Events[I].JSONType = jtObject then begin

        Event := Events.Objects[I];

        if Event.Find('x', XJson) and Event.Find('y', YJson) then begin
          X := XJson.AsInteger;
          Y := YJson.AsInteger;

          EventRect := Rect(
            X * DstTileWidth + 1,
            Y * DstTileHeight + 1,
            (X + 1) * DstTileWidth -2,
            (Y + 1) * DstTileHeight -2
          );

          MapPaintBox.Canvas.Rectangle(EventRect);

          MapPaintBox.Canvas.TextRect(EventRect, 0, 0, IntToStr(Event.Integers['id']));
        end;
      end;
end;

function TMapViewFrame.FindEventAt(X, Y: Integer): Integer;
var
  Events: TJSONArray = nil;
  Event: TJSONObject;
  XJson: TJSONNumber = nil;
  YJson: TJSONNumber = nil;
  I: Integer;
begin
  if (Map <> nil) and Map.Find('events', Events) then
    for I := 1 to Events.Count - 1 do
      if Events[I].JSONType = jtObject then begin
        Event := Events.Objects[I];

        if Event.Find('x', XJson) and Event.Find('y', YJson) then begin
          if (XJson.AsInteger = X) and (YJson.AsInteger = Y) then begin
            FindEventAt := I;
            Exit
          end;
        end;
      end;

  FindEventAt := 0;
end;

procedure TMapViewFrame.SetMode(AValue: TMapViewFrameMode);
begin
  if FMode = AValue then Exit;
  FMode := AValue;
  if AValue = mvfmSelectLocation then
    SelectMapLayer(LAYER_EVENTS);
end;

procedure TMapViewFrame.UpdateMapImage;
  var
    RectShifted: TRect;
    RectInTiles: TRect;
    NewMapImage: TBGRABitmap;
  begin
    if (Drawer = nil) or (Map = nil) or (Db = nil) or (TilesetGraphics = nil) then
      Exit;

    RectShifted := GetShiftedClientRect;
    RectInTiles := GetMapRectInTiles;

    MapImageX := RectInTiles.Left * DstTileWidth;
    MapImageY := RectInTiles.Top * DstTileHeight;
    NewMapImage := Drawer.DrawMap(Map, DstTileWidth, DstTileHeight, RectInTiles);

    if MapImage <> nil then begin
      MapImage.Free;
      MapImage := nil;
    end;

    OldClientRect := RectShifted;
    MapImage := NewMapImage;
end;

procedure TMapViewFrame.UpdatePartOfMapImage(PartInTiles: TRect);
var
  RectInTiles, RectToDraw: TRect;
  PartImage: TBGRABitmap;
begin
  if MapImage = nil then
    Exit;

  RectInTiles := GetMapRectInTiles;

  RectToDraw.Create(RectInTiles);
  RectToDraw.Intersect(PartInTiles);

  if RectToDraw.IsEmpty then
    Exit;

  PartImage := Drawer.DrawMap(Map, DstTileWidth, DstTileHeight, RectToDraw);
  MapImage.CanvasBGRA.Draw(
    (RectToDraw.Left - RectInTiles.Left) * DstTileWidth,
    (RectToDraw.Top - RectInTiles.Top) * DstTileHeight,
    PartImage
  );

  PartImage.Free;
end;

procedure TMapViewFrame.SetZoom(Zoom: Integer);
begin
  if Zoom < Low(Zooms) then
    Zoom := Low(Zooms);
  if Zoom > High(Zooms) then
    Zoom := High(Zooms);

  DstTileWidth := Zooms[Zoom]; //TODO: allow tiles of different width
  DstTileHeight := Zooms[Zoom];

  if Map <> nil then
    UpdateMapSize;
end;

constructor TMapViewFrame.Create(TheOwner: TComponent);
begin
  inherited;
  DstTileWidth := 48; //TODO: allow tiles of different width
  DstTileHeight := 48;
  Drawer := TMapDrawer.Create;
  IsDrawing := False;

  FEventCursorX := -1;
  FEventCursorY := -1;
end;

procedure TMapViewFrame.SetMap(AMap: TJSONObject; ATilesetGraphics: TTilesetGraphics; ADb: TDatabase);
begin
  Map := AMap;
  Db := ADb;
  TilesetGraphics := ATilesetGraphics;
  IsDrawing := False;

  FEventCursorX := -1;
  FEventCursorY := -1;

  if Map <> nil then begin
    UpdateMapSize(False);
    Drawer.SetTileset(ATilesetGraphics, Db);
  end;
  UpdateMapImage;
  Refresh;
end;

procedure TMapViewFrame.UpdateMapSize(NeedsRepaint: Boolean = True);
var
  WidthJson, HeightJson: TJSONNumber;
begin
  if Map.Find('width', WidthJson) and Map.Find('height', HeightJson) then begin
    MapWidth := WidthJson.AsInteger;
    MapHeight := HeightJson.AsInteger;
    MapPaintBox.Width := MapWidth * DstTileWidth;
    MapPaintBox.Height := MapHeight * DstTileHeight;
    (* Left can be reset by FlipChildren (for RTL layout). Technically
      small maps should be right-aligned but I had problems with
      ClientWidth - Width so let's keep it left-aligned for now *)
    MapPaintBox.Left := 0;

    MapPaintBox.Visible := True;
    if NeedsRepaint then begin
      UpdateMapImage;
      Refresh;
    end;
  end
  else begin
    MapWidth := 0;
    MapHeight := 0;
    MapPaintBox.Visible := False;
  end;
end;

destructor TMapViewFrame.Destroy;
begin
  if Drawer <> nil then
    Drawer.Free;
  if MapImage <> nil then
    MapImage.Free;
  inherited;
end;

procedure TMapViewFrame.SetSelection(ASelection: TTileSelection);
begin
  (* We don't free the selection because it is handled in the TMainForm. *)
  Selection := ASelection;
end;

procedure TMapViewFrame.SelectMapLayer(LayerId: Integer);
var
  OldLayerId: Integer;
begin
  OldLayerId := SelectedLayer;
  SelectedLayer := LayerId;

  if (SelectedLayer = LAYER_EVENTS) <> (OldLayerId = LAYER_EVENTS) then
    Refresh;
end;

procedure TMapViewFrame.UpdateLanguage(Code: String);
begin
  IsRTL := IsLanguageRtl(Code);

  MapScrollBox.FlipChildren(False);

  if IsRTL then
    MapScrollBox.BiDiMode := bdRightToLeft
  else
    MapScrollBox.BiDiMode := bdLeftToRight;

  MapPaintBox.Left := 0;
  if (Map <> nil) and (MapImage <> nil) then
    UpdateMapSize(True);
end;

end.

